﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using StyleMVVM.Data.Activation;

namespace StyleMVVM.DependencyInjection.Impl
{
	public interface IAutomaticFuncFactory
	{
		object CreateFactory(IDependencyInjectionContainer container);
	}

	internal class AutomaticFuncFactory<T> : IAutomaticFuncFactory
	{
		private static readonly MethodInfo containerLocateWithContext;
		private static readonly MethodInfo containerCreateInjectionContext;
		private static readonly MethodInfo injectionContextAdd;

		static AutomaticFuncFactory()
		{
			foreach (MethodInfo declaredMethod in typeof(IDependencyInjectionContainer).GetTypeInfo().GetDeclaredMethods("CreateInjectionContext"))
			{
				if (declaredMethod.GetParameters().Length == 0)
				{
					containerCreateInjectionContext = declaredMethod;
					break;
				}
			}

			containerLocateWithContext =
				typeof(AutomaticFuncFactory<T>).GetTypeInfo().GetDeclaredMethod("LocateByTypeWithContext");

			foreach (MethodInfo declaredMethod in typeof(IInjectionContext).GetTypeInfo().GetDeclaredMethods("RegisterExport"))
			{
				var parameters = declaredMethod.GetParameters();

				if (parameters.Length == 2 &&
					parameters[0].ParameterType == typeof(string) &&
					parameters[1].ParameterType == typeof(object))
				{
					injectionContextAdd = declaredMethod;
					break;
				}
			}
		}

		public object CreateFactory(IDependencyInjectionContainer container)
		{
			Type tType = typeof(T);

			if (!tType.IsConstructedGenericType || tType.GetGenericTypeDefinition().Name == "Func`")
			{
				throw new Exception("Must be Func<...>");
			}

			return CreateFuncFromTypes(container, tType.GenericTypeArguments);
		}
		
		private object CreateFuncFromTypes(IDependencyInjectionContainer container, Type[] typeParameters)
		{
			bool createdInjectionContext = false;
			ParameterExpression injectionContextExpression = null;
			List<ParameterExpression> parameterExpressions = new List<ParameterExpression>();
			List<Expression> methodExpressions = new List<Expression>();

			for (int i = 0; i < typeParameters.Length - 1; i++)
			{
				ParameterExpression expression =
					Expression.Parameter(typeParameters[i]);

				parameterExpressions.Add(expression);

				if (typeParameters[i] == typeof(IInjectionContext))
				{
					injectionContextExpression = expression;
				}
			}

			if (injectionContextExpression == null)
			{
				createdInjectionContext = true;

				injectionContextExpression = Expression.Variable(typeof(IInjectionContext));

				Expression callMethod = Expression.Call(Expression.Constant(container), containerCreateInjectionContext);

				methodExpressions.Add(Expression.Assign(injectionContextExpression,callMethod));
			}

			for (int i = 0; i < typeParameters.Length - 1; i++)
			{
				if (injectionContextExpression != parameterExpressions[i])
				{
					methodExpressions.Add(Expression.Call(injectionContextExpression,
					                                      injectionContextAdd,
					                                      Expression.Constant(typeParameters[i].FullName),
					                                      parameterExpressions[i]));
				}
			}

			MethodInfo closedLocated = containerLocateWithContext.MakeGenericMethod(typeParameters.Last());
			
			methodExpressions.Add(
				Expression.Convert(
					Expression.Call(closedLocated,
										 Expression.Constant(container), 
										 injectionContextExpression),
				   typeParameters.Last()));

			BlockExpression bodyBlock = null;

			if (createdInjectionContext)
			{
				bodyBlock = Expression.Block(new[] { injectionContextExpression }, methodExpressions);
			}
			else
			{
				bodyBlock = Expression.Block(new ParameterExpression[0], methodExpressions);
			}

			return Expression.Lambda<T>(bodyBlock,parameterExpressions).Compile();
		}

		private static object LocateByTypeWithContext<TService>(IDependencyInjectionContainer container, IInjectionContext context)
		{
			return container.LocateByTypeWithContext(context, typeof(TService));
		}
	}
}
